/*
 ============================================================================
 Name        : OrgaCompus.c
 Author      : 
 Version     :
 Copyright   : Your copyright notice
 ============================================================================
 */


#include <unistd.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>

#define LONGITUD_BLOQUE_BINARIO 3
#define LONGITUD_BLOQUE_BASE_64 4
#define CANTIDAD_BITS_BASE64 6
#define LONGITUD_TABLA 64
#define PADDING '='

static char encoding_table[] = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
                                'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
                                'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
                                'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
                                'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
                                'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
                                'w', 'x', 'y', 'z', '0', '1', '2', '3',
                                '4', '5', '6', '7', '8', '9', '+', '/'};


/* Devuelve un numero de un byte con un 1 en el lugar que indique indice_bit, empezando desde el LSB */
unsigned char calcularMascaraBit(int indice_bit) {
        int i;
        unsigned char mask = 1;
        for (i = 0 ; i < indice_bit; i++)
                mask = mask << 1;
        return mask;
}

/* Devuelve un numero de un byte con la cantidad de 1's que indique cantidad_bits, empezando desde el LSB */
unsigned char calcularMascaraLSB (int cantidad_bits) {
        if (cantidad_bits > 8) return 0;
        int i;
        unsigned char mask = 0;
        for (i = cantidad_bits - 1; i >= 0; i--) {
                mask += 1;
                mask = mask << ((i == 0)? 0 : 1);
        }
        return mask;
}

/* Devuelve un numero de un byte con la cantidad de 1's que indique cantidad_bits, empezando desde el MSB */
unsigned char calcularMascaraMSB (int cantidad_bits) {
        if (cantidad_bits > 8) return 0;
        unsigned char mask = calcularMascaraLSB(cantidad_bits);
        mask = mask << (8 - cantidad_bits);
        return mask;
}

/* Funcion que shiftea un array de chars, sin perder datos
 * @params: len es la longitud del bloque binario
 */
void shiftRightCustom(unsigned char * array, int cantidad_shifts) {
        if (cantidad_shifts == 0) return;
        int i;
        unsigned char lsb_actual = 0;
        unsigned char lsb_anterior = 0;
        for (i = 0; i < LONGITUD_BLOQUE_BINARIO ; i++) {
                lsb_actual = array[i] & calcularMascaraLSB(cantidad_shifts);
                array[i] = array[i] >> cantidad_shifts;
                array[i] += lsb_anterior;
                lsb_anterior = lsb_actual << (8 - cantidad_shifts);
        }
}

void shiftLeftCustom(unsigned char * array, int cantidad_shifts) {
        if (cantidad_shifts == 0) return;
        int i;
        unsigned char msb_actual = 0;
        unsigned char msb_anterior = 0;
        for (i = LONGITUD_BLOQUE_BASE_64 - 1; i >= 0; i--) {
                msb_actual = array[i] & calcularMascaraMSB(cantidad_shifts);
                array[i] = array[i] << cantidad_shifts;
                array[i] += msb_anterior;
                msb_anterior = msb_actual >> (8 - cantidad_shifts);
        }
}


/*
 * Obtiene valores en base 2. Array original contiene los valores en base 64.
 */
void obtenerValoresBinarios(unsigned char * valores_binarios, unsigned char * array_orig) {
        char byte = 0;
        int i, indice_byte;
        for (indice_byte = 0; indice_byte < LONGITUD_BLOQUE_BASE_64; indice_byte++) {
                for (i = 0; i < CANTIDAD_BITS_BASE64; i++) {
                        shiftLeftCustom(valores_binarios, 1);
                        byte += array_orig[indice_byte] & calcularMascaraBit(CANTIDAD_BITS_BASE64 - 1 - i);
                        byte = byte >> (CANTIDAD_BITS_BASE64 - 1 - i);

                        valores_binarios[LONGITUD_BLOQUE_BINARIO - 1] += byte;
                        byte = 0;
                }
        }
}

/*
 * Obtiene valores en base64. Array original contiene los valores en binario.
 */
void obtenerValoresBase64(unsigned char * valores_base, unsigned char * array_orig) {
        int i;
        for (i = 0; i <= LONGITUD_BLOQUE_BINARIO; i++) {
                shiftRightCustom(array_orig, (i == 0)? 0 : CANTIDAD_BITS_BASE64);
                //En la primera operacion no shifteo, en las demas shifteo 6
                valores_base[LONGITUD_BLOQUE_BASE_64 - 1 - i] = array_orig[LONGITUD_BLOQUE_BINARIO - 1]
                                                                & calcularMascaraLSB(6);
        }
}


unsigned char * transformacionBase64ABinario(unsigned char * array_orig, int len) {
        unsigned char* binario = malloc (sizeof (unsigned char) * LONGITUD_BLOQUE_BINARIO);
        obtenerValoresBinarios(binario, array_orig);
        return binario;
}

unsigned char * transformacionBinarioABase64(unsigned char * array_orig, int len) {
        unsigned char * porcion_indices_tabla = malloc (sizeof (unsigned char) * LONGITUD_BLOQUE_BASE_64);
        obtenerValoresBase64(porcion_indices_tabla, array_orig);
        return porcion_indices_tabla;
}

/* Dado un caracter devuelve true en caso que el mismo pertenezca a la tabla de encoding */
bool tablaContieneCaracter(char c) {
	int i;
	for (i = 0; i < LONGITUD_TABLA; i++)
		if (c == encoding_table[i]) return true;
	return false;
}

/* Dado un caracter devuelve el indice correspondiente en la tabla de encoding */
int obtenerIndices(char c) {
	int i;
	for (i = 0; i < LONGITUD_TABLA; i ++){
		if (c == encoding_table[i]) return i;
	}
	return -1;
}

/* Funcion que recibe un array con indices y devuelve un array con los simbolos correspondientes */
void transformarConTabla(unsigned char * buf, int len) {
	int i;
	for (i = 0; i < len; i++) {
		buf[i] = encoding_table[(int)buf[i]];
	}
}

/* Funcion que recibe un array con letras en base 64 y devuelve un array con los indices correspondientes
 * Retorna la cantidad de bytes destransformados. Si se encuentra con un padding corta. Si hay un
 * caracter que no existe en la tabla retorna -1
 * */
int destransformarConTabla(unsigned char * buf, int len) {
	int i;
	int indice;
	for (i = 0; i < len; i++) {
		if (buf[i] == PADDING) return i;
		indice = obtenerIndices(buf[i]);
		if (indice < 0) return -1;
		buf[i] = indice;
	}
	return len;
}


/* Agrega el símbolo de padding */
unsigned char * agregarPaddingDeBinarioABase64(unsigned char * buf, int i) {
	int y = i;
	for (; y < LONGITUD_BLOQUE_BINARIO; y++){
		buf[y + 1] = PADDING;
	}
	return buf;
}


void imprimir(FILE * f_output, unsigned char * buf, int len) {
	int i;
	for (i = 0; i < len; i++){
		fputc(buf[i], f_output);
		//printf("%d", buf[i]);
	}
	//printf("\n");
}

//Devuelve 0 si un bloque tiene un padding en el medio.
int validarBloque(unsigned char* buf, int length){
	int i = 0;
	for (i = 0; i < length - 1; i++ ){
		if (buf[i] == PADDING){
			//Chequeo el siguiente char
			if (buf[i+1] != PADDING)
				return 0;
		}
	}
	return 1;
}

/* Funcion que recibe un archivo de entrada y otro de salida. Toma de a 4 bytes, transforma de base 64 a binario*/
void decode(FILE* f_input, FILE* f_output) {
	int i = 0;
	int termino_con_padding = 0;
	unsigned char* buffer_transformado;
	int char_del_archivo = fgetc(f_input);
	unsigned char buf[LONGITUD_BLOQUE_BASE_64];
	while (!feof(f_input)) {
		buf[i] = char_del_archivo;
		i++;
		if (i == LONGITUD_BLOQUE_BASE_64) {
			i = 0;
			if (!validarBloque(buf, LONGITUD_BLOQUE_BASE_64) || (termino_con_padding && buf[0] != PADDING)){
				fputs("El bloque tiene padding erroneo\n", f_output);
				return;
			}
			termino_con_padding = (char_del_archivo == PADDING)? 1 : 0;
			int cant_exitos = destransformarConTabla(buf, LONGITUD_BLOQUE_BASE_64);
			if (cant_exitos < 0) {
				fputs("El bloque tiene un caracter erroneo\n", f_output);
				return;
			}
			buffer_transformado = transformacionBase64ABinario(buf, cant_exitos - 1);
			imprimir(f_output, buffer_transformado, cant_exitos - 1);
			free(buffer_transformado);
		}
		char_del_archivo = fgetc(f_input);
	}

	if (i != 0) { //Si es distinto de cero...
		fputs("El bloque tiene una longitud incorrecta\n", f_output);
		return;
	}
}


/* Funcion que recibe dos archivos. Toma 3 bytes, los transforma de binario a base 64
 * y los coloca en el archivo de salida */
void encode(FILE* f_input, FILE* f_output) {
	int i = 0;
	unsigned char* buffer_transformado;
	int char_del_archivo = fgetc(f_input);
	unsigned char buf[LONGITUD_BLOQUE_BINARIO];
	while (!feof(f_input)) {
		buf[i] = char_del_archivo;
		i++;
		if (i == LONGITUD_BLOQUE_BINARIO) {
			i = 0;
			buffer_transformado = transformacionBinarioABase64(buf,
			LONGITUD_BLOQUE_BINARIO);
			transformarConTabla(buffer_transformado, LONGITUD_BLOQUE_BASE_64);
			imprimir(f_output, buffer_transformado, LONGITUD_BLOQUE_BASE_64);
			free(buffer_transformado);
		}
		char_del_archivo = fgetc(f_input);
	}
	if (i != 0) { //Si es distinto de cero hay que poner padding
		int cantidad_de_bytes = i;
		for (; i < LONGITUD_BLOQUE_BINARIO; i++)
			buf[i] = 0;
		buffer_transformado = transformacionBinarioABase64(buf,
				LONGITUD_BLOQUE_BINARIO);
		transformarConTabla(buffer_transformado, LONGITUD_BLOQUE_BASE_64);
		agregarPaddingDeBinarioABase64(buffer_transformado, cantidad_de_bytes);
		imprimir(f_output, buffer_transformado, LONGITUD_BLOQUE_BASE_64);
		free(buffer_transformado);
	}
}


/* Chequea los argumentos recibidos */
void getArgs(int argc, char* argv[]) {

	FILE * f_input;
	FILE * f_output;

	if (argc == 1) {

		printf("No options given\n");
		return;

	}

	bool dflag = false;
	bool hflag = false;
	bool vflag = false;
	bool eflag = false;
	bool iflag = false;
	bool oflag = false;

	char *ivalue = NULL;
	char *ovalue = NULL;

	int c;

	while ((c = getopt(argc, argv, "vhedi:o:")) != -1) {

		switch (c) {

		case 'v':
			vflag = true;
			break;

		case 'h':
			hflag = true;
			break;

		case 'e':
			eflag = true;
			break;

		case 'd':
			dflag = true;
			break;

		case 'i':
			iflag = true;
			ivalue = optarg;
			break;

		case 'o':
			oflag = true;
			ovalue = optarg;
			break;

		case '?':

			if (optopt == 'i') {

				return;

			} else if (optopt == 'o') {

				return;

			}

		}

	}

	if (vflag) {

		printf("tp0 for 6620-Orga de compus. 1ro C 2014 \n");
	}

	if (hflag) {

		printf(
				"OPTIONS: \n -e --encode Encodes to Base64 \n -d --decode Decodes from base64 \n -i --input file Reads from file or stdin \n -o --output file Writes to file or stdout \n -v --version Show version string \n -h --help Print this message and quit \n");
	}

	if (iflag) {

		f_input = fopen(ivalue, "rb");
		if (ferror(f_input)) {
			fprintf(stderr, "Error in file..\n");

		}
	} else {
		fflush(stdin);
		f_input = stdin;
	}

	if (oflag) {

		f_output = fopen(ovalue, "w");
		if (ferror(f_output)) {
			fprintf(stderr, "Error in file..\n");
		}


	} else {
		fflush(stdout);
		f_output = stdout;
	}

	if (eflag && !dflag) {

		encode(f_input, f_output);
	}

	if (dflag && !eflag) {

		decode(f_input, f_output);
	}
	fclose(f_input);
	fclose(f_output);
}

int main(int argc, char* argv[]) {

	getArgs(argc, argv);
    return EXIT_SUCCESS;
}
